﻿using System;
using System.Net;
using System.ComponentModel;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.Serialization;
using System.Collections.Generic;
using System.Linq;

namespace D20RpgCommon.Util
{
    public partial class Observable : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged = delegate { };

        private IDictionary<PropertyInfo, IDictionary<Delegate, List<PropertyChangedEventHandler>>> _handlerWrappers =
            new Dictionary<PropertyInfo, IDictionary<Delegate, List<PropertyChangedEventHandler>>>();

        public IDisposable HandlePropertyChanges<T>(
            Expression<Func<T>> propertyExpression, Action<T> handler)
        {
            var property = getPropertyInfo(propertyExpression);
            if (property == null)
            {
                throw new InvalidOperationException(
                    "Invalid property specified.");
            }
            else
            {
                if (!_handlerWrappers.ContainsKey(property))
                {
                    _handlerWrappers[property] =
                        new Dictionary<Delegate, List<PropertyChangedEventHandler>>();
                }
                if (!_handlerWrappers[property].ContainsKey(handler))
                {
                    _handlerWrappers[property][handler] =
                        new List<PropertyChangedEventHandler>();
                }

                PropertyChangedEventHandler newEventHandler = (s, e) =>
                {
                    if (e.PropertyName == property.Name)
                    {
                        handler((T)property.GetValue(this, null));
                    }
                };
                _handlerWrappers[property][handler].Add(newEventHandler);
                PropertyChanged += newEventHandler;

                return new Observation(
                    () => PropertyChanged -= newEventHandler);
            }
        }

        public void StopHandlingPropertyChanges<T>(
            Expression<Func<T>> propertyExpression, Action<T> handler)
        {
            var property = getPropertyInfo(propertyExpression);
            if (property != null)
            {
                _handlerWrappers[property][handler].ForEach(
                    h => PropertyChanged -= h);
                _handlerWrappers[property].Remove(handler);
            }
        }

        public void SetAndNotify<T>(
            Expression<Func<T>> propertyExpression, T value)
        {
            var property = getPropertyInfo(propertyExpression);
            if (property == null)
            {
                throw new ArgumentException(
                    "The provided expression is not a property expression",
                    "propertyExpression");
            }
            else
            {
                T oldValue = (T)property.GetValue(this, null);
                if (oldValue == null && value != null
                    || !oldValue.Equals(value))
                {
                    // Set the property
                    property.SetValue(this, value, null);

                    // Notify that the property has changed
                    PropertyChanged(
                        this, new PropertyChangedEventArgs(property.Name));
                }
            }
        }

        private PropertyInfo getPropertyInfo<T>(
            Expression<Func<T>> propertyExpression)
        {
            var memberExpression = propertyExpression.Body as MemberExpression;
            return memberExpression == null
                ? null : memberExpression.Member as PropertyInfo;
        }

        private class Observation : IDisposable
        {
            private Action _disposeAction;

            public Observation(Action disposeAction)
            {
                _disposeAction = disposeAction;
            }

            public void Dispose()
            {
                _disposeAction();
            }
        }
    }
}
